#!/usr/bin/env python3
"""
GitHub Releases 最新发行版批量下载脚本
支持并行下载多个仓库，每个仓库内的文件顺序下载
"""

import json
import os
import sys
import asyncio
import aiohttp
import fnmatch
from pathlib import Path
from typing import List, Dict, Tuple
from urllib.parse import urlparse


class GitHubReleaseDownloader:
    """GitHub Release下载器"""
    
    def __init__(self, config_file: str = "config/repos.json", download_dir: str = "downloads"):
        self.config_file = config_file
        self.download_dir = Path(download_dir)
        self.download_dir.mkdir(parents=True, exist_ok=True)
        self.github_token = os.getenv("GITHUB_TOKEN")
        
    def load_config(self) -> Dict:
        """加载配置文件"""
        try:
            with open(self.config_file, 'r', encoding='utf-8') as f:
                config = json.load(f)
            print(f"✓ 成功加载配置文件: {self.config_file}")
            print(f"✓ 找到 {len(config)} 个仓库")
            return config
        except FileNotFoundError:
            print(f"✗ 配置文件不存在: {self.config_file}")
            sys.exit(1)
        except json.JSONDecodeError as e:
            print(f"✗ 配置文件格式错误: {e}")
            sys.exit(1)
    
    def parse_repo_config(self, repo: str, config_value) -> Tuple[List[str], str, str]:
        """解析仓库配置，返回 (文件列表, 版本类型, 重命名)"""
        # 兼容旧格式：直接是数组
        if isinstance(config_value, list):
            return config_value, "latest", ""
        
        # 新格式：对象格式
        if isinstance(config_value, dict):
            files = config_value.get("files", [])
            release_type = config_value.get("type", "latest").lower()
            rename = config_value.get("rename", "")
            
            # 验证 release_type
            if release_type not in ["latest", "pre-release"]:
                print(f"⚠ 警告: 仓库 {repo} 的 type 值 '{release_type}' 无效，使用默认值 'latest'")
                release_type = "latest"
            
            return files, release_type, rename
        
        print(f"⚠ 警告: 仓库 {repo} 的配置格式无效，跳过")
        return [], "latest", ""
    
    def get_headers(self) -> Dict[str, str]:
        """获取请求头"""
        headers = {
            "Accept": "application/vnd.github+json",
            "User-Agent": "GitHub-Release-Downloader"
        }
        if self.github_token:
            headers["Authorization"] = f"Bearer {self.github_token}"
        return headers
    
    async def get_release(self, session: aiohttp.ClientSession, repo: str, release_type: str = "latest") -> Dict:
        """获取仓库的release信息
        
        Args:
            session: aiohttp会话
            repo: 仓库名称 (owner/repo)
            release_type: 版本类型，"latest" 或 "pre-release"
        """
        if release_type == "latest":
            # 获取最新的正式版本
            url = f"https://api.github.com/repos/{repo}/releases/latest"
        else:
            # 获取最新的版本（包括pre-release）
            url = f"https://api.github.com/repos/{repo}/releases"
        
        try:
            async with session.get(url, headers=self.get_headers()) as response:
                if response.status == 404:
                    print(f"✗ 仓库 {repo} 没有找到或没有release")
                    return None
                elif response.status != 200:
                    print(f"✗ 获取 {repo} 的release失败: HTTP {response.status}")
                    return None
                
                data = await response.json()
                
                # 如果是获取所有releases，取第一个（最新的）
                if release_type == "pre-release":
                    if isinstance(data, list) and len(data) > 0:
                        return data[0]
                    else:
                        print(f"✗ 仓库 {repo} 没有可用的release")
                        return None
                
                return data
        except Exception as e:
            print(f"✗ 获取 {repo} 的release时发生错误: {e}")
            return None
    
    def match_assets(self, assets: List[Dict], patterns: List[str]) -> List[Dict]:
        """根据通配符模式匹配资源文件"""
        matched = []
        for pattern in patterns:
            for asset in assets:
                if fnmatch.fnmatch(asset['name'], pattern):
                    if asset not in matched:
                        matched.append(asset)
        return matched
    
    async def download_file(self, session: aiohttp.ClientSession, asset: Dict, target_dir: Path, custom_filename: str = "") -> bool:
        """下载单个文件
        
        Args:
            session: aiohttp会话
            asset: 资源文件信息
            target_dir: 目标目录
            custom_filename: 自定义文件名（可选），为空则使用原始文件名
        """
        url = asset['browser_download_url']
        original_filename = asset['name']
        filename = custom_filename if custom_filename else original_filename
        file_path = target_dir / filename
        
        # 检查文件是否已存在
        if file_path.exists():
            existing_size = file_path.stat().st_size
            if existing_size == asset['size']:
                if custom_filename:
                    print(f"  ✓ 文件已存在且大小匹配，跳过: {original_filename} -> {filename}")
                else:
                    print(f"  ✓ 文件已存在且大小匹配，跳过: {filename}")
                return True
            else:
                if custom_filename:
                    print(f"  ! 文件已存在但大小不匹配，重新下载: {original_filename} -> {filename}")
                else:
                    print(f"  ! 文件已存在但大小不匹配，重新下载: {filename}")
        
        try:
            if custom_filename:
                print(f"  ⬇ 正在下载: {original_filename} -> {filename} ({self.format_size(asset['size'])})")
            else:
                print(f"  ⬇ 正在下载: {filename} ({self.format_size(asset['size'])})")
            
            async with session.get(url) as response:
                if response.status != 200:
                    print(f"  ✗ 下载失败: {filename} (HTTP {response.status})")
                    return False
                
                # 创建临时文件
                temp_file = file_path.with_suffix(file_path.suffix + '.tmp')
                
                with open(temp_file, 'wb') as f:
                    downloaded = 0
                    total = asset['size']
                    chunk_size = 8192
                    
                    async for chunk in response.content.iter_chunked(chunk_size):
                        f.write(chunk)
                        downloaded += len(chunk)
                        
                        # 显示进度
                        if total > 0:
                            progress = (downloaded / total) * 100
                            print(f"  📊 进度: {progress:.1f}% ({self.format_size(downloaded)}/{self.format_size(total)})", end='\r')
                
                # 下载完成，重命名文件
                temp_file.rename(file_path)
                print(f"\n  ✓ 下载完成: {filename}")
                return True
                
        except Exception as e:
            print(f"  ✗ 下载 {filename} 时发生错误: {e}")
            # 清理临时文件
            temp_file = file_path.with_suffix(file_path.suffix + '.tmp')
            if temp_file.exists():
                temp_file.unlink()
            return False
    
    async def download_repo_releases(self, session: aiohttp.ClientSession, repo: str, patterns: List[str], release_type: str = "latest", rename: str = "") -> None:
        """下载指定仓库的release文件"""
        print(f"\n{'='*60}")
        print(f"📦 处理仓库: {repo}")
        print(f"{'='*60}")
        
        # 显示版本类型
        type_desc = "正式版本" if release_type == "latest" else "预发行版本（包括pre-release）"
        print(f"🎯 版本类型: {type_desc}")
        
        # 获取release
        release = await self.get_release(session, repo, release_type)
        if not release:
            return
        
        tag_name = release.get('tag_name', 'unknown')
        release_name = release.get('name', tag_name)
        is_prerelease = release.get('prerelease', False)
        prerelease_tag = " [Pre-release]" if is_prerelease else ""
        print(f"✓ 找到最新版本: {release_name} ({tag_name}){prerelease_tag}")
        
        # 匹配资源文件
        assets = release.get('assets', [])
        if not assets:
            print(f"✗ 该release没有可下载的文件")
            return
        
        matched_assets = self.match_assets(assets, patterns)
        if not matched_assets:
            print(f"✗ 没有匹配的文件")
            print(f"  可用的文件: {', '.join([a['name'] for a in assets])}")
            return
        
        print(f"✓ 匹配到 {len(matched_assets)} 个文件:")
        for asset in matched_assets:
            print(f"  - {asset['name']} ({self.format_size(asset['size'])})")
        
        # 如果指定了重命名但匹配到多个文件，给出警告
        if rename and len(matched_assets) > 1:
            print(f"⚠ 警告: 指定了 rename 但匹配到 {len(matched_assets)} 个文件，只有第一个文件会被重命名")
        
        # 顺序下载文件
        success_count = 0
        for idx, asset in enumerate(matched_assets):
            # 只对第一个文件应用重命名
            custom_name = rename if (rename and idx == 0) else ""
            if await self.download_file(session, asset, self.download_dir, custom_name):
                success_count += 1
        
        print(f"\n✓ 仓库 {repo} 完成: 成功 {success_count}/{len(matched_assets)} 个文件")
    
    @staticmethod
    def format_size(size: int) -> str:
        """格式化文件大小"""
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size < 1024.0:
                return f"{size:.2f} {unit}"
            size /= 1024.0
        return f"{size:.2f} TB"
    
    async def run(self):
        """运行下载任务"""
        print("🚀 GitHub Releases 批量下载器")
        print("=" * 60)
        
        # 加载配置
        config = self.load_config()
        
        # 创建会话
        timeout = aiohttp.ClientTimeout(total=3600)  # 1小时超时
        async with aiohttp.ClientSession(timeout=timeout) as session:
            # 并行处理所有仓库
            tasks = []
            for repo, config_value in config.items():
                # 解析配置
                patterns, release_type, rename = self.parse_repo_config(repo, config_value)
                
                if not patterns:
                    print(f"⚠ 警告: 仓库 {repo} 没有配置文件模式，跳过")
                    continue
                
                task = self.download_repo_releases(session, repo, patterns, release_type, rename)
                tasks.append(task)
            
            # 等待所有任务完成
            await asyncio.gather(*tasks)
        
        print("\n" + "=" * 60)
        print("✓ 所有下载任务完成！")
        print(f"✓ 文件保存在: {self.download_dir.absolute()}")


def main():
    """主函数"""
    # 可以通过环境变量配置
    config_file = os.getenv("CONFIG_FILE", "config/repos.json")
    download_dir = os.getenv("DOWNLOAD_DIR", "downloads")
    
    downloader = GitHubReleaseDownloader(config_file, download_dir)
    
    try:
        asyncio.run(downloader.run())
    except KeyboardInterrupt:
        print("\n\n⚠ 用户中断下载")
        sys.exit(1)
    except Exception as e:
        print(f"\n✗ 发生错误: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()

